/*
 * Galaxium Messenger
 * 
 * Copyright (C) 2008 Paul Burton <paulburton89@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Reflection;
using System.Threading;
using System.Web.Services;
using System.Web.Services.Protocols;

using Anculus.Core;

using Galaxium.Core;

namespace Galaxium.Protocol.Msn.Soap
{
	public class MsnSoapService : SoapHttpClientProtocol
	{
		IConfigurationSection _config = Configuration.Protocol.Section["MSN"];
		bool _forceContentType = false;
		int _timeout;
		int _attempts;
		MsnSession _session;
		List<string> _urls = new List<string> ();
		
		delegate object[] InvokeDelegate (string method_name, object[] parameters, bool saveState);
		Dictionary<IAsyncResult, InvokeDelegate> _asyncDelegates = new Dictionary<IAsyncResult, InvokeDelegate> ();
		
		protected bool ForceContentType
		{
			set { _forceContentType = value; }
		}
		
		public MsnSession Session
		{
			get { return _session; }
		}
		
		public IEnumerable<string> Urls
		{
			get { return _urls; }
		}
		
		public MsnSoapService (MsnSession session)
		{
			UserAgent = SoapConstants.UserAgent;
			AllowAutoRedirect = true;
			
			_session = session;
			
			_timeout = _config.GetInt ("SOAPTimeout", 10000);
			_attempts = _config.GetInt ("SOAPAttempts", 10);
			
			Timeout = _timeout;
		}
		
		protected override WebRequest GetWebRequest (Uri uri)
		{
			WebRequest request = base.GetWebRequest (uri);
			
			if (request.RequestUri.OriginalString.Contains ("https://"))
			{
				if (Configuration.Proxy.Section.GetBool (Configuration.Proxy.UseHttpsProxy.Name, Configuration.Proxy.UseHttpsProxy.Default))
				{
					string host = Configuration.Proxy.Section.GetString (Configuration.Proxy.HttpsHost.Name, Configuration.Proxy.HttpsHost.Default);
					int port = Configuration.Proxy.Section.GetInt (Configuration.Proxy.HttpsPort.Name, Configuration.Proxy.HttpsPort.Default);
					string username = Configuration.Proxy.Section.GetString (Configuration.Proxy.HttpsUsername.Name, Configuration.Proxy.HttpsUsername.Default);
					string password = Configuration.Proxy.Section.GetString (Configuration.Proxy.HttpsPassword.Name, Configuration.Proxy.HttpsPassword.Default);
					
					(request as HttpWebRequest).Proxy = new WebProxy (host, port);
					(request as HttpWebRequest).Proxy.Credentials = new NetworkCredential (username, password);
				}
			}
			else if (request.RequestUri.OriginalString.Contains ("http://"))
			{
				if (Configuration.Proxy.Section.GetBool (Configuration.Proxy.UseHttpProxy.Name, Configuration.Proxy.UseHttpProxy.Default))
				{
					string host = Configuration.Proxy.Section.GetString (Configuration.Proxy.HttpHost.Name, Configuration.Proxy.HttpHost.Default);
					int port = Configuration.Proxy.Section.GetInt (Configuration.Proxy.HttpPort.Name, Configuration.Proxy.HttpPort.Default);
					string username = Configuration.Proxy.Section.GetString (Configuration.Proxy.HttpUsername.Name, Configuration.Proxy.HttpUsername.Default);
					string password = Configuration.Proxy.Section.GetString (Configuration.Proxy.HttpPassword.Name, Configuration.Proxy.HttpPassword.Default);
					
					(request as HttpWebRequest).Proxy = new WebProxy (host, port);
					(request as HttpWebRequest).Proxy.Credentials = new NetworkCredential (username, password);
				}
			}
			
			return request;
		}
		
		protected override WebResponse GetWebResponse (WebRequest request)
		{
			request.Timeout = _timeout;
			(request as HttpWebRequest).AllowAutoRedirect = true;
			(request as HttpWebRequest).KeepAlive = false;
			
			return new MsnSoapWebResponse (base.GetWebResponse (request), _forceContentType ? "text/xml" : null);
		}

		protected override WebResponse GetWebResponse (WebRequest request, IAsyncResult result)
		{
			request.Timeout = _timeout;
			(request as HttpWebRequest).AllowAutoRedirect = true;
			(request as HttpWebRequest).KeepAlive = false;
			
			return new MsnSoapWebResponse (base.GetWebResponse (request, result), _forceContentType ? "text/xml" : null);
		}
		
		protected virtual object[] SaveState ()
		{
			List<object> state = new List<object> ();
			
			foreach (FieldInfo field in this.GetType ().GetFields ())
			{
				if (field.FieldType.IsSubclassOf (typeof (SoapHeader)) && (field.FieldType.GetInterface ("ICloneable") != null))
				{
					object header = field.GetValue (this);
					
					Log.Debug ("Saving cloned header {0}", field.FieldType.Name);
					
					if (header != null)
					{
						// Clone the header and save it
						state.Add ((header as ICloneable).Clone ());
					}
					else
						state.Add (null);
				}
			}
			
			return state.ToArray ();
		}
		
		protected virtual void LoadState (object[] state)
		{
			int i = 0;
			
			foreach (FieldInfo field in this.GetType ().GetFields ())
			{
				if (field.FieldType.IsSubclassOf (typeof (SoapHeader)) && (field.FieldType.GetInterface ("ICloneable") != null))
				{
					Log.Debug ("Restoring header {0}", field.FieldType.Name);
					
					field.SetValue (this, state[i++]);
				}
			}
		}
		
		protected virtual bool HandleFault (SoapException ex)
		{
			return false;
		}
		
		protected virtual object[] Invoke (string method_name, object[] parameters, bool saveState)
		{
			foreach (RequireSecurityTokensAttribute att in this.GetType ().GetMethod (method_name).GetCustomAttributes (typeof (RequireSecurityTokensAttribute), true))
				_session.RequireSecurityTokens (att.Domains);
			
			object[] state = saveState ? SaveState () : new object[0];
			
			for (int attempt = 0; attempt < _attempts; attempt++)
			{
				DateTime startTime = DateTime.Now;
				
				if (saveState && (attempt > 0))
					LoadState (state);
				
				if (string.IsNullOrEmpty (Url))
					SwitchUrl ();
				
				try
				{
					Log.Debug ("Invoking {0} (attempt {1} of {2}) on {3}", method_name, attempt + 1, _attempts, Url);
					object[] ret = base.Invoke (method_name, parameters);
					Log.Debug ("Invoke {0} on {1} successful on attempt {2}", method_name, Url, attempt + 1);
					return ret;
				}
				catch (TargetException ex)
				{
					// Skip any further attempts
					attempt = _attempts - 1;
					
					throw ex;
				}
				catch (Exception ex)
				{
					if ((ex is SoapException) && !HandleFault (ex as SoapException))
					{
						// Skip any further attempts
						attempt = _attempts - 1;
					}
					
					// Ensure we wait at least the timeout before retrying
					
					Log.Warn (ex, "Error invoking {0} on {1}", method_name, Url);
					
					if (attempt == _attempts - 1)
						throw ex;
					
					if ((ex is WebException) && !(ex is SoapException))
						SwitchUrl ();
					
					if ((DateTime.Now - startTime).TotalMilliseconds < _timeout - 100)
					{
						int delay = _timeout - (int)(DateTime.Now - startTime).TotalMilliseconds;
						Log.Debug ("Waiting {0}ms before retrying", delay);
						Thread.Sleep (delay);
					}
				}
			}
			
			throw new ApplicationException ("Unknown SOAP invoke error");
		}
		
		public new virtual object[] Invoke (string method_name, object[] parameters)
		{
			return Invoke (method_name, parameters, false);
		}
		
		public new IAsyncResult BeginInvoke (string method_name, object[] parameters, AsyncCallback callback, object asyncState)
		{
			// We have to use our own delegate so that our own Invoke is called rather than base.Invoke which doesn't
			// retry requests
			
			InvokeDelegate del = new InvokeDelegate (Invoke);
			IAsyncResult asyncResult = del.BeginInvoke (method_name, parameters, true, delegate (IAsyncResult result)
			{
				ThreadUtility.SyncDispatch (new VoidDelegate (delegate
				{
					callback (result);
				}));
			}, asyncState);
			
			_asyncDelegates.Add (asyncResult, del);
			
			return asyncResult;
		}
		
		public new object[] EndInvoke (IAsyncResult asyncResult)
		{
			if (!_asyncDelegates.ContainsKey (asyncResult))
			{
				Log.Error ("EndInvoke called with an invalid asyncResult");
				return null;
			}
			
			Exception ex = null;
			object[] ret = null;
			
			try
			{
				ret = _asyncDelegates[asyncResult].EndInvoke (asyncResult);
			}
			catch (Exception e)
			{
				ex = e;
			}
			
			_asyncDelegates.Remove (asyncResult);
			
			if (ex != null)
				throw ex;
			
			return ret;
		}
		
		protected void AddUrl (string url)
		{
			_urls.Add (url);
		}
		
		protected void SwitchUrl ()
		{
			int i = _urls.IndexOf (Url) + 1;
			
			if ((i == 0) || (i >= _urls.Count))
				i = 0;
			
			SwitchUrl (_urls[i]);
		}
		
		protected void SwitchUrl (string url)
		{
			if (!_urls.Contains (url))
				AddUrl (url);
			
			if (Url == url)
				return;
			
			Log.Debug ("Switching to Url {0}", url);
			
			Url = url;
		}
	}		
}
